Functional Programming in Vim

„Huh?“ you ask. Yes, it is actually possible to do functional programming in Vim. Of course this is not as convenient as in a language which is actually designed for functional programming like Haskell or Clojure. But nevertheless: it is possible!

Closures

A main building block for functional programming are closures. Consider for example this clojure snippet.

(let [x 5
      y 6]
  (future
    (+ x y)))

Under the hood, Clojure creates a thunk containing the body of the future call, that is (+ x y). This thunk is passed to a freshly created Future object, which sets up a new thread and calls the thunk in the new thread. Since the thunk is a closure it remembers the environment where it was defined. So although the thunk is executed in a different thread, it has access to the locals x and y.

Closures in Vim

Now Vim doesn't have closures. You can define function objects, but they don't remember their environment. However we can manually set up a poor man's closure by virtue of Vim's maps and 1st order functions.

That means, that we have to manually close over all values from the environment, which we later on need, when executing the function. To do that, we simply set up a map containing the required values. Plus the function.

function CounterBump() dict
    let cnt = self.counter
    let self.counter += 1
    return cnt
endfunction

function MakeCounter(initial)
    let closure = { counter: a:initial }
    let closure.bump = function("MakeCounterBump")
    return closure
endfunction

MakeCounter returns a closure, which „closes over the local counter variable“. It should pretty obvious what happens. The function CounterBump accesses a well defined set of key in the map to do its work. The MakeCounter constructor sets up the required keys and adds the function to the map.

Let's use our new counter.

let counter1 = MakeCounter(5)
counter1.bump() " gives 5
counter1.bump() " gives 6
let counter2 = MakeCounter(0)
counter2.bump() " gives 0
counter1.bump() " gives 7

This different calls to MakeCounter give different maps. Hence the CounterBump function sees different counter locals.

Application Example

Suppose you want to move some text from one buffer to another. This could be done be yanking the text into a register and paste into the target buffer. However, what happens in that case with the register content? Maybe the user saved already something there?

So we have to protect the register's content. This adds a lot of annoying boilerplate. Save the content, do things, restore the register.

This can be hidden away in with the above technique. Consider the following helper.

function WithSavedRegister(register, closure)
    let savedReg = getreg(a:register)
    let result = closure.f()
    call setreg(a:register, savedReg)
    return result
endfunction

The usage should be fairly obvious by now. We first define a worker function. Then create our „closure“ map with a well defined f key and all other necessary things. Finally we pass everything to our WithSavedRegister utility to actually do the work.

function Worker() dict
    " do stuff here
    return "something useful"
endfunction
let closure = { f: function("Worker") }
let somethingUseful = WithSavedRegister("x", closure)

Ooops. But what happens if an exception is thrown in our Worker? The register will not be restored. If we had sprinkled our code with such boilerplate, we would now be in trouble. We'd have to go to each and everyplace and add the protection. But with our utility it is sufficient to fix WithSavedRegister.

function WithSavedRegister(register, closure)
    let savedReg = getreg(a:register)
    try
        let result = closure.f()
    finally
        call setreg(a:register, savedReg)
    endtry
    return result
endfunction

Upshot

Functional programming in Vim is possible, although it is not really supported out of the box. Lightweight objects in form of maps and function pointers make this possible.

Published by Meikel Brandmeyer on .